// Connect to HAL over D-Bus
// Copyright 2006 (C) Ralph Thomas

#include <apps/photo/camera_monitor.h>
#include <boost/weak_ptr.hpp>
#include <gtk/gtk.h>
#include <dbus/dbus.h>
#include <dbus/dbus-glib.h>
#include <dbus/dbus-glib-lowlevel.h>
#include <libhal.h>
#include <string>
#include <set>
#include <iostream>

namespace apps { namespace photo {

struct camera_monitor_impl_t {
	typedef boost::signal<void ()>		change_signal_t;
	typedef std::set<std::string>		udi_list_t;
	DBusConnection*	dbus_connection_m;
	LibHalContext*	hal_context_m;
	change_signal_t	add_signal_m;
	change_signal_t	remove_signal_m;
	udi_list_t	udi_list_m;

	static gboolean _send_add_signal( void* user_data ) {
		camera_monitor_impl_t* us( reinterpret_cast<camera_monitor_impl_t*>( user_data ) );
		us->add_signal_m();
		return false;
	}

	static void hal_device_added( LibHalContext* hal_ctx, const char* udi ) {
		//
		// If it has the camera capability then hopefully libgphoto2
		// will be able to import from it.
		//
		camera_monitor_impl_t* us( reinterpret_cast<camera_monitor_impl_t*>( libhal_ctx_get_user_data( hal_ctx ) ) );
	
		if (!libhal_device_query_capability (hal_ctx, udi, "camera", NULL))
			return;
		//
		// Check that we haven't already scheduled an add message for
		// this one.
		//
		udi_list_t::iterator i = us->udi_list_m.find( udi );
		if( i == us->udi_list_m.end() ) {
			us->udi_list_m.insert( udi );
			//
			// We wait for the device to settle down. There's
			// probably some way to do this from a HAL message
			// rather than having a flakey timeout. If we send
			// the add signal from here then libgphoto2 doesn't
			// see the camera.
			//
			g_timeout_add( 1000, _send_add_signal, us );
		}
	}

	static void hal_device_removed( LibHalContext* hal_ctx, const char* udi ) {
		camera_monitor_impl_t* us( reinterpret_cast<camera_monitor_impl_t*>( libhal_ctx_get_user_data( hal_ctx ) ) );
		//
		// If we know this UDI to be a camera then issue a removal signal.
		//
		udi_list_t::iterator i( us->udi_list_m.find( udi ) );
		if( i == us->udi_list_m.end() ) return;
		us->remove_signal_m();
		us->udi_list_m.erase( i );
	}

	static gboolean _reinit_dbus( void* user_data ) {
		camera_monitor_impl_t* us( reinterpret_cast<camera_monitor_impl_t*>( user_data ) );
		us->hal_context_m = us->_hal_init();
		if( us->hal_context_m ) return false;
		//
		// D-Bus not available again yet.
		//
		return true;
	}

	static DBusHandlerResult _dbus_filter( DBusConnection* conn, DBusMessage* msg, void* user_data ) {
		camera_monitor_impl_t* us( reinterpret_cast<camera_monitor_impl_t*>( user_data ) );
		if( dbus_message_is_signal( msg, DBUS_INTERFACE_LOCAL, "Disconnected" ) && strcmp( dbus_message_get_path( msg ), DBUS_PATH_LOCAL ) == 0 ) {
			libhal_ctx_free( us->hal_context_m );
			us->hal_context_m = 0;

			dbus_connection_unref( us->dbus_connection_m );
			us->dbus_connection_m = 0;

			g_timeout_add( 3000, _reinit_dbus, user_data );

			return DBUS_HANDLER_RESULT_HANDLED;
		}
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	bool _dbus_init() {
		DBusError error;

		if( dbus_connection_m ) return true;
		dbus_error_init( &error );
		if( !(dbus_connection_m = dbus_bus_get( DBUS_BUS_SYSTEM, &error ) ) ) {
			dbus_error_free( &error );
			return false;
		}
		dbus_bus_add_match( dbus_connection_m, "sender='org.freedesktop.Hal'", NULL );
		dbus_bus_add_match( dbus_connection_m, "sender='org.freedesktop.Hal.Manager'", NULL );
		dbus_connection_setup_with_g_main( dbus_connection_m, 0 );
		dbus_connection_set_exit_on_disconnect( dbus_connection_m, false );
		dbus_connection_add_filter( dbus_connection_m, _dbus_filter, this, 0 );

		return true;
	}

	LibHalContext* _hal_init() {
		LibHalContext*	ctx;
		DBusError	error;
		char**		devices;
		int		nr;

		if( !_dbus_init() ) return 0;

		if( !(ctx = libhal_ctx_new()) ) return 0;

		libhal_ctx_set_dbus_connection( ctx, dbus_connection_m );
		libhal_ctx_set_user_data( ctx, this );
		libhal_ctx_set_device_added( ctx, hal_device_added );
		libhal_ctx_set_device_removed( ctx, hal_device_removed );

		dbus_error_init( &error );
		if( !libhal_device_property_watch_all( ctx, &error ) ) {
			std::cerr << "failed to watch all HAL properties: " << error.message << std::endl;
			dbus_error_free( &error );
			libhal_ctx_free( ctx );
			return 0;
		}

		if( !libhal_ctx_init( ctx, &error ) ) {
			std::cerr << "failed to watch all HAL properties: " << error.message << std::endl;
			dbus_error_free( &error );
			libhal_ctx_free( ctx );
			return 0;
		}

		//
		// The above functions don't contact the HAL daemon. We need
		// to do something that will make sure it's actually running.
		//
		if( !(devices = libhal_get_all_devices( ctx, &nr, &error ) ) ) {
			std::cerr << "HAL isn't running: " << error.message << std::endl;
			dbus_error_free( &error );
			libhal_ctx_shutdown( ctx, NULL );
			libhal_ctx_free( ctx );
			return 0;
		}
		libhal_free_string_array( devices );

		return ctx;
	}

	camera_monitor_impl_t() : dbus_connection_m( 0 ), hal_context_m( 0 ) {
		hal_context_m = _hal_init();
	}
	~camera_monitor_impl_t() {
		if( hal_context_m )	libhal_ctx_free( hal_context_m );
		if( dbus_connection_m )	dbus_connection_unref( dbus_connection_m );
	}
	camera_monitor_t::connection_t on_camera_add( const camera_monitor_t::change_func_t& fn ) {
		return add_signal_m.connect( fn );
	}
	camera_monitor_t::connection_t on_camera_remove( const camera_monitor_t::change_func_t& fn ) {
		return remove_signal_m.connect( fn );
	}
};

camera_monitor_t::camera_monitor_ptr_t camera_monitor_t::get_instance() {
	//
	// Use a weak pointer so that we don't keep the camera_monitor
	// in memory longer than we have to.
	//
	static boost::weak_ptr<camera_monitor_t> instance;
	camera_monitor_t::camera_monitor_ptr_t actual( instance.lock() );
	if( actual ) return actual;
	actual = camera_monitor_t::camera_monitor_ptr_t( new camera_monitor_t() );
	instance = actual;
	return actual;
}

camera_monitor_t::camera_monitor_t() : impl_m( new camera_monitor_impl_t() ) {}
camera_monitor_t::~camera_monitor_t() {
	delete impl_m;
}
camera_monitor_t::connection_t camera_monitor_t::on_camera_add( const camera_monitor_t::change_func_t& fn ) {
	return impl_m->on_camera_add( fn );
}
camera_monitor_t::connection_t camera_monitor_t::on_camera_remove( const camera_monitor_t::change_func_t& fn ) {
	return impl_m->on_camera_remove( fn );
}

} }

